package resml

import (
	"encoding/base64"
	"errors"
	"fmt"
	"io"
	"io/fs"
	"net"
	"net/url"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/hirochachacha/go-smb2"
)

// Samba 会话缓存
type sambaSessionCache map[string]*SambaSession

// 创建一个会话缓存
var sambaSessions = make(sambaSessionCache)

// 从会话缓存中，根据 URI 获得或创建 Samba Session，返回会话对象与该URI在服务器上的文件名。
// 如果 error 不为 nil，则 Samba Session 一定为 nil。
func (c sambaSessionCache) Get(uri *url.URL) (*SambaSession, string, error) {
	usr := uri.User
	if usr == nil {
		return nil, "", fmt.Errorf("invalid samba url, missing user info: %s", uri)
	}
	shared, fn, _ := strings.Cut(strings.Trim(strings.ReplaceAll(uri.Path, "\\", "/"), "/ \t\r\n"), "/")
	fn = "/" + fn
	id := fmt.Sprintf("smb://%s@%s/%s", usr.String(), uri.Host, shared)
	if ss, exists := c[id]; exists && ss != nil {
		return ss, fn, nil
	}
	if ss, err := NewSambaSession(uri, shared); err == nil {
		c[id] = ss
		return ss, fn, nil
	} else {
		return nil, "", err
	}
}

// 从默认的会话缓存中，根据 URI 获得或创建 Samba Session，返回会话对象与该 URI 在服务器上的文件名。
// 如果 error 不为 nil，则 Samba Session 一定为 nil。
func GetSambaSession(uri *url.URL) (*SambaSession, string, error) {
	return sambaSessions.Get(uri)
}

// Samba 客户端
type SambaClient struct {
	host     string // 服务器地址
	port     int    // 端口（默认 445）
	username string // 账号名
	password string // 密码
}

// 通过 URI 创建一个 Samba 客户端。
// URI 需要包含用户名/密码等认证消息，例如 smb://user:name@host:port。
func NewSambaClient(uri *url.URL) (*SambaClient, error) {
	if uri == nil {
		return nil, errors.New("can not create samba client, the uri is nil")
	}
	if uri.User == nil {
		return nil, errors.New("can not create samba client, the user info in uri is nil")
	}
	pwd, _ := uri.User.Password()
	port := 445
	if p := uri.Port(); p != "" {
		if pn, err := strconv.Atoi(p); err == nil {
			port = pn
		}
	}
	return &SambaClient{
		host:     uri.Hostname(),
		port:     port,
		username: uri.User.Username(),
		password: pwd,
	}, nil
}

// 客户端ID
func (sc *SambaClient) Id(withUserInfo bool) string {
	if withUserInfo {
		return fmt.Sprintf("smb://%s:%s@%s:%d", sc.username, sc.password, sc.host, sc.port)
	}
	return fmt.Sprintf("smb://%s:%d", sc.host, sc.port)
}

// 创建新的会话
func (sc *SambaClient) NewSession() (*smb2.Session, error) {
	conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", sc.host, sc.port))
	if err != nil {
		return nil, err
	}
	dailer := &smb2.Dialer{Initiator: &smb2.NTLMInitiator{
		User:     sc.username,
		Password: sc.password,
	}}
	sess, err := dailer.Dial(conn)
	if err != nil {
		return nil, fmt.Errorf("samba dialer dial error: %v", err)
	}
	return sess, nil
}

// 挂载指定的共享目录
func (sc *SambaClient) MountDir(sess *smb2.Session, dir string) (*smb2.Share, error) {
	fs, err := sess.Mount(dir)
	i := 0
	for i < 5 && err != nil {
		time.Sleep(time.Millisecond * 200)
		fs, err = sess.Mount(dir)
		i++
	}
	if err != nil {
		return nil, fmt.Errorf("samba mount dir(%s) error (%d tries): %v", dir, i, err)
	}
	return fs, nil
}

// Samba 会话
type SambaSession struct {
	client  *SambaClient      // 客户端
	session *smb2.Session     // 底层会话对象
	sdir    string            // 共享目录名
	share   *smb2.Share       // 共享目录对象
	mgr     *SambaTempFileMgr // 临时文件的管理器
}

// 通过 URI 与指定的共享目录 shared 创建一个会话。
// URI 需要包含用户名/密码等认证消息，例如 smb://user:name@host:port/SharedDir；
// shared 是共享目录的名称，如果为空，则使用 URI 路径的第一段（如例中的 SharedDir）。
// 如果无法找到 shared 则返回 error。
func NewSambaSession(uri *url.URL, shared string) (*SambaSession, error) {
	sdir := strings.Trim(strings.TrimSpace(shared), "/")
	if sdir == "" {
		sdir = strings.Trim(strings.TrimSpace(uri.Path), "/")
		if sdir == "" {
			return nil, errors.New("can not create samba session, the shared dir is empty")
		}
	}
	client, err := NewSambaClient(uri)
	if err != nil {
		return nil, err
	}
	return &SambaSession{client: client, sdir: sdir}, nil
}

// 确保会话就绪，然后执行 fn，如果网络错误，则重新创建会话再执行一次。
func (s *SambaSession) do(fn func() error) (err error) {
	if s.session == nil || s.share == nil {
		if err = s.mount(); err != nil {
			return
		}
	}
	if err = fn(); err == nil {
		return // 成功执行，马上返回
	}
	// 不是网络错误，不需要重试了，直接返回
	if _, ok := err.(*fs.PathError); ok {
		return err
	} else if _, ok := err.(*os.PathError); ok {
		return err
	}
	// 可能是网络错误，卸载后重新创建会话，再执行
	s.share.Umount()
	s.session.Logoff()
	if err = s.mount(); err != nil {
		return
	}
	return fn()
}

// 创建新会话并挂载共享目录
func (s *SambaSession) mount() (err error) {
	s.session, err = s.client.NewSession()
	if err != nil {
		return
	}
	s.share, err = s.client.MountDir(s.session, s.sdir)
	if err != nil {
		s.session.Logoff()
		s.session = nil
	}
	return
}

// 添加临时文件管理器
func (s *SambaSession) WithTempFileMgr(mgr *SambaTempFileMgr) error {
	if s.sdir == "" {
		return errors.New("samba session with temp file manager error: shared dir is empty")
	}
	s.mgr = mgr.WithSession(s)
	return s.mgr.StartCleanJob()
}

// 获取指定文件的元数据，其中的 etag 按指定的 etager 生成。
func (s *SambaSession) Meta(name string, etager ETager) (meta Meta, err error) {
	edo := s.do(func() error {
		if e := s.withTempFileMgrIfNil(); e != nil { // 设置缓存管理器
			return e
		}
		_, meta, err = s.mgr.Cache(s.name(name), etager)
		return nil
	})
	if edo != nil {
		return nil, edo
	}
	return
}

// 读取指定的文件，其中返回的 etag 按指定的 etager 生成。
func (s *SambaSession) Get(name string, etager ETager) (reader io.ReadCloser, meta Meta, err error) {
	edo := s.do(func() error {
		if e := s.withTempFileMgrIfNil(); e != nil { // 设置缓存管理器
			return e
		}
		var cacheName string
		if cacheName, meta, err = s.mgr.Cache(s.name(name), etager); err != nil {
			return nil // 将 edo 设置为 nil，将 (nil, meta, err) 作为最终返回值
		}
		reader, err = os.Open(cacheName)
		return nil // 将 edo 设置为 nil，将 (reader, meta, err) 作为最终返回值
	})
	if edo != nil {
		return nil, nil, edo
	}
	return
}

// 读取文件，返回该文件的 reader。
func (s *SambaSession) ReadFile(name string) (reader io.ReadCloser, err error) {
	edo := s.do(func() error {
		reader, err = s.share.Open(s.name(name))
		return nil
	})
	if edo != nil {
		return nil, fmt.Errorf("samba read file(%s) error: %v", name, edo)
	}
	return
}

// 从 reader 读取内容，然后创建/覆盖写入到文件名为 name 的文件中
func (s *SambaSession) WriteFile(name string, reader io.Reader) (n int64, err error) {
	edo := s.do(func() error {
		fn := s.name(name)
		if e := s.share.MkdirAll(filepath.Dir(fn), 0755); e != nil {
			return e
		}
		fd, e := s.share.OpenFile(fn, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
		if e != nil {
			return e
		}
		defer fd.Close()
		n, err = io.Copy(fd, reader)
		return nil
	})
	if edo != nil {
		return 0, fmt.Errorf("samba write file(%s) error: %v", name, edo)
	}
	return
}

// 从 reader 读取内容，然后创建/追加写入到文件名为 name 的文件中
func (s *SambaSession) AppendFile(name string, offset int64, reader io.Reader) (n int64, err error) {
	edo := s.do(func() error {
		fn := s.name(name)
		if e := s.share.MkdirAll(filepath.Dir(fn), 0755); e != nil {
			return e
		}
		fd, e := s.share.OpenFile(fn, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
		if e != nil {
			return e
		}
		defer fd.Close()
		if offset > 0 {
			fd.Seek(offset, io.SeekStart)
		}
		n, err = io.Copy(fd, reader)
		return nil
	})
	if edo != nil {
		return 0, fmt.Errorf("samba append file(%s) at %d error: %v", name, offset, edo)
	}
	return
}

// 返回文件的大小、修改时间并判断是否为目录。
func (s *SambaSession) Stat(name string) (fi fs.FileInfo, err error) {
	edo := s.do(func() error {
		fi, err = s.share.Stat(s.name(name))
		// 注：此处 do(fn) 中的 fn 必须返回 nil，因为：
		// 在 fn 之外的 do 如果出错（edo），则返回错误，否则将 fi, err 作为最终返回结果
		return nil
	})
	if edo != nil {
		err = fmt.Errorf("samba stat file(%s) error: %v", name, edo)
	}
	return
}

// 删除文件/目录
func (s *SambaSession) Remove(name string) (err error) {
	edo := s.do(func() error {
		return s.share.Remove(s.name(name))
	})
	if edo != nil {
		err = fmt.Errorf("samba remove file(%s) error: %v", name, edo)
	}
	return
}

// 文件名称
func (s *SambaSession) name(n string) string {
	return strings.Trim(n, "/\\ \t\r\n")
}

func (s *SambaSession) withTempFileMgrIfNil() error {
	if s.mgr != nil { // 设置缓存管理器
		return nil
	}
	if mgr, err := NewSambaTempFileMgr(os.TempDir(), 10*time.Minute); err == nil {
		s.WithTempFileMgr(mgr)
		return nil
	} else {
		return err
	}
}

// Samba 临时文件管理器
type SambaTempFileMgr struct {
	root string // 根目录
	tdir string // 临时目录，对应一个 SessionClient 与 Shared Dir

	cleanInterval time.Duration // 清理临时文件的间隔
	cleanTicker   *time.Ticker
	session       *SambaSession
}

// 创建临时文件管理器
func NewSambaTempFileMgr(root string, cleanInterval time.Duration) (*SambaTempFileMgr, error) {
	if root == "" {
		root = os.TempDir()
	}
	if err := os.MkdirAll(root, 0750); err != nil {
		return nil, err
	}
	stat, err := os.Stat(root)
	if err != nil {
		return nil, err
	}
	if !stat.IsDir() {
		return nil, fmt.Errorf("create samba temp file manager error: root is not a dir: %s", root)
	}
	if cleanInterval < time.Second*10 {
		cleanInterval = time.Second * 10
	}
	return &SambaTempFileMgr{root: root, cleanInterval: cleanInterval}, nil
}

// 设置共享目录
func (m *SambaTempFileMgr) WithSession(sess *SambaSession) *SambaTempFileMgr {
	m.session = sess
	m.tdir = base64.RawURLEncoding.EncodeToString([]byte(fmt.Sprintf("%s/%s", sess.client.Id(false), sess.sdir)))
	return m
}

// 缓存文件，返回本地名称、本地meta文件
func (m *SambaTempFileMgr) Cache(name string, etager ETager) (cacheName string, meta Meta, err error) {
	if m.session == nil {
		err = errors.New("can not cache file, session is nil")
		return
	}
	rfi, err := m.session.Stat(name)
	if err != nil {
		return // 查看远程文件状态出错，马上返回
	}
	cacheName = m.localName(name)
	metaName := m.metaName(cacheName)
	if lfi, e := os.Stat(cacheName); e == nil && !lfi.IsDir() && rfi.Size() == lfi.Size() { // 本地文件存在
		if meta, err = MetaOfHttpHeaderTextFile(metaName); err == nil { // 元数据文件也存在
			if meta.ContentLength() == rfi.Size() && SecondEquals(meta.LastModified(), rfi.ModTime()) {
				// 本地文件与远端文件一致，说明已经有正确的缓存，马上返回
				return
			}
		}
	} else if e == nil && lfi.IsDir() {
		return // 目录，不缓存，马上返回
	}
	// 下载并缓存文件
	r, err := m.session.ReadFile(name)
	if err != nil {
		return // 打开远程文件失败
	}
	defer r.Close()
	fd, err := FileCreate(cacheName, false)
	if err != nil {
		return // 创建本地失败
	}
	defer fd.Close()
	if etager == nil {
		etager = NewSha256HexEtager()
	}
	meta, err = MetaOfCopy(fd, r, etager, rfi.ModTime(), filepath.Ext(name))
	if err != nil {
		return // 生成元数据失败
	}
	if err = os.WriteFile(metaName, []byte(MetaHttpHeaderText(meta)), 0644); err != nil {
		return // 保存元数据失败
	}
	return
}

// 开始清理任务
func (m *SambaTempFileMgr) StartCleanJob() error {
	m.StopCleanJob()
	m.cleanTicker = time.NewTicker(m.cleanInterval)
	go func() {
		for range m.cleanTicker.C {
			m.cleanJob()
		}
	}()
	return nil
}

// 结束清理任务
func (m *SambaTempFileMgr) StopCleanJob() error {
	if m.cleanTicker != nil {
		m.cleanTicker.Stop()
	}
	return nil
}

// 清理临时目录
func (m *SambaTempFileMgr) cleanJob() {
	if m.session == nil {
		return
	}
	filepath.WalkDir(filepath.Join(m.root, m.tdir), func(path string, d fs.DirEntry, err error) error {
		if m.session == nil || err != nil || d.IsDir() {
			return err
		}
		name, err := base64.RawURLEncoding.DecodeString(d.Name())
		if err != nil {
			return nil // 忽略
		}
		rfi, err := m.session.Stat(string(name))
		if err != nil {
			if os.IsNotExist(err) {
				os.Remove(path) // 删除
			}
			return nil // 忽略
		}
		if rfi.IsDir() {
			os.Remove(path) // 远程是目录，但本地是文件，删除
		}
		return nil
	})
}

// 本地文件名
func (m *SambaTempFileMgr) localName(name string) string {
	return filepath.Join(m.root, m.tdir, base64.RawURLEncoding.EncodeToString([]byte(m.session.name(name))))
}

// 元数据文件名
func (m *SambaTempFileMgr) metaName(localName string) string {
	return localName + ".meta"
}
